/**
 * 可编辑的 HTML 表格.
 *
 * let table = EditableTable.install (options);
 *
 * 选项:
 *   container              string|jqObject        必须, 一个jquery表达式或jquery对象，指定为表格的容器
 *   columns                number                 可选，初始化列数，默认为5
 *   rows                   number                 可选, 初始化行数，默认 0
 *   commands               Array<String>          可选，每行后面的操作按钮，默认显示所有按钮。只能在以下列表中取值
 *                                                      ['insert', 'remove', 'move-up', 'move-down', 'move-top', 'move-bottom']
 *                                                      可以在options中传入 {commands:null}来取消显示按钮
 *   columnClasses          Array<String>          可选，分别被应用于每列的CSS Class 名称
 *   highlightClassName     String
 *   data                   Array<Array>           可选，二维数组，用于初始化表格，若无，则初始化出一个指定行列的空表格
 *
 *   columnTypes                                   可选，若该选项不提供，认为数据类型为 'text'
 *                          Array<string>               对应列的数据类型，目前仅支持 'text'
 *                          Array<Array<string>>        对应单元格的数据类型，目前仅支持 'text'
 *                          function                    传入row和column，返回这个单元格的数据类型
 *   tableColumns           Array<Object>          可选  列定义，其中对象为配置对象，格式为：
 *                                                       header       string     表示列头
 *                                                       className    string     css class
 *                                                       type         string     默认的列类型
 *   selectable             Boolean                可选   行是否可选
 *
 *   getCellEditor          function(row, column)  可选   指定一个单元格的编辑器
 *                                                       row    - 行号
 *                                                       column - 列号
 *                                                       参见类： Editor
 *
 *   getCellRenderer        function(row, column)   可选  指定一个单元格的渲染器
 *                                                       row    - 行号
 *                                                       column - 列号
 *                                                       参见类： Renderer
 *
 * 方法：
 *   addRow ()
 *       在表格末尾添加空行
 *
 *   appendData (data)
 *       data - 二维数组，用于追加的数据
 *
 *   highlightRows (...rows)
 *       rows - 需要高亮的行号
 *
 *   clearHighlight (all | ...rows)
 *       all  - 若第一个参数为 true (且仅为 true时），清除全表的高亮
 *       rows - 需要清除的行号
 *
 *   getModel ()
 *       返回当前表格的模型
 *
 *   dispose ()
 *       销毁表格
 *
 *   getCellType (row, column)
 *       row - 指定行
 *       column - 指定列
 *
 *       返回指定行列的数据类型
 *
 *   getValue ()
 *       返回表格的值，返回值为一个二维数组 或 null
 *
 *   setData (data)
 *       data - 二维数组，用于填充的数据
 *
 *       调用该方法，将清空原有数据，并使用传入的 data 填出数据，若需要在表格底部添加数据，应调用 appendData (data)
 *
 *
 * 类：TableModel - 表格的数据模型。用于将表格和数据模型进行关联，并决定表格的行为
 *
 *   方法：
 *      isCellEditable (row, column)    - 指示单元格 (row, column)是否可编辑
 *          row    - 行号
 *          column - 列号
 *          returns: 如果单元格可编辑，返回true，否则返回false
 *
 *      getRowCount ()                  - 返回表格数据的行数
 *
 *      getColumnCount ()               - 返回表格数据的列数
 *
 *      getValueAt (row, column)        - 返回单元格的值
 *          row    - 行号
 *          column - 列号
 *          returns: 返回指定单元格对应的值
 *
 *      setValueAt (row, column, value) - 设置指定单元格的值
 *          row    - 行号
 *          column - 列号
 *
 *      setData (data)                  - 设置数据模型
 *          data   - 数据模型
 *
 *      fireTableDataChanged ()         - 将数据模型的变动更新到表格
 *---------------------------------------------------------------------------------------------------------------
 * 类： AbstractEditor - 抽象的编辑器.
 *   如果需要扩展自定义的单元格编辑器，通常由这个类来扩展
 *   方法：
 *      abstract getComponent ()              - 获取编辑器的可视化控件，必须是一个包裹 DOM 元素的 jQuery 对象
 *      abstract getValue ()                  - 获取编辑器的结果值
 *      abstract setValue (value)             - 设置编辑器的当前值
 *      fit (parent)                          - 通过某些计算，使得 this.getComponent () 返回的控件刚好符合 parent 的尺寸
 *                                               parent - this.getComponent() 返回的控件即将要插入的父元素，jQuery 对象
 *
 *   事件：
 *      onopen ()                             - 当 this.getComponent() 返回的控件即将被插入到父元素时触发
 *      onclose ()                            - 当 this.getComponent() 返回的控件被从父元素移除前触发
 *
 * 类：TextEditor extend AbstractEditor        - 文本编辑器的具体实现
 *
 * 类：SelectableEditor extends AbstractEditor - 下拉框编辑器的具体实现
 *   构造函数：
 *      SelectableEditor(dict)                - 使用一个字典对象来构建一个下拉框编辑器，字典的内容将会被用于下拉框的选项，其中
 *                                              字典的键值将是不可见的，字典值将显示在下拉框的选项中。
 *
 *   这个类实现的 setValue(value) 和 getValue() 也会根据这个字典对值进行翻译，例：
 *   var dict = { male: '男', female: '女' };
 *   var editor = new SelectableEditor (dict);
 *   在编辑器控件中所有可见的都将是 dict 的值，即 '男' 或 '女'，儿 setValue (value)
 *   的参数将传递的是 dict 的键，即 'male' 或 'female'; getValue () 返回的也是 dict 的键.
 *---------------------------------------------------------------------------------------------------------------
 * 类：AbstractRenderer - 抽象的渲染器
 *   如果需要扩展自定义的单元格渲染器，通常由这个类来扩展
 *   方法：
 *      abstract getComponent ()              - 获取渲染器真正用于显示值的可视化控件，必须是包裹 DOM 元素的 jQuery 对象
 *      abstract setValue (value)             - 将当前单元格值复制给渲染器
 *
 * 类：private TextRenderer - 默认的基础单元格渲染器，使用原始文本来显示单元格内容
 *
 * 类: SelectableRenderer extends AbstractRenderer
 *   这是和 SelectableEditor 所对应的渲染器。它和 SelectableEditor 一样遵循同样的字典逻辑.
 *
 * 类：BooleanRenderer extends AbstractRenderer
 *   专门用于 boolean 类型数据的渲染器，它会将 boolean 类型数据显示为一个 选中 或 未选中 的 checkbox。
 *   实际实，boolean 类型的渲染器同时也是 boolean 类型的数据编辑器。
 *---------------------------------------------------------------------------------------------------------------
 * 各个类之间的逻辑关系：
 *  - TableModel 负责维护数据
 *  - Renderer 用于每个单元格的显示，通过扩展 Renderer 可以自定义每个单元格的显示方式，数据由 TableModel.getValueAt(row, column)
 *    方法提供。
 *  - Editor 用于每个单元格数据的编辑，通过扩展 Editor 可自定义每个单元格的编辑方式，数据由 TableModel.getValueAt(row, column)提供。
 *    编辑完成后，由 TableModel.setValueAt (row, column, value) 方法将数据同步到数据源。
 *  - EditableTable 负责数据的显示，属于 ui 层，通过 getCellEditor(row, column) 方法获取单元格的编辑器；
 *    通过 getCellRenderer (row, column)方法获取单元格的渲染器来显示数据.
 *---------------------------------------------------------------------------------------------------------------
 *
 * Created with IntelliJ IDEA.
 * User: seth.yang
 * Date: 14-10-28
 * Time: 下午12:23
 * To change this template use File | Settings | File Templates.
 */
if (!window.EditableTable) window.EditableTable = (function ($) {
    let KEY_CODE = {RETURN: 13, ESCAPE: 27, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40};
    let defaults = {
            columns: 5, rows: 0,
            commands : ['insert', 'remove', 'move-up', 'move-down', 'move-top', 'move-bottom']
        };
    let tables = {}, handler = {holder: $('<div/>')},
        mask = $('<div class="et-mask"/>').css ({width:$(window).width (), height:$(window).height ()}),
        prompt = $('<div class="et-prompt"/>'), message;

    $(function () {
        mask.appendTo ($(document.body));
        prompt.text ('您确定要删除该行吗？').appendTo ($(document.body));
        message = $('<p />').appendTo (prompt);
        let panel = $('<div/>').appendTo (prompt);
        $('<a class="et-button et-button-ok"/>').text ('删除').on ('click', function () {
            let e = handler ['event'], me = e.table, tr = e.row;
            me.removeRow (tr.rowIndex - me.start);
            hidePrompt();
        }).appendTo (panel);
        $('<a class="et-button et-button-cancel"/>').text ('取消').on ('click', function () {
            hidePrompt ();
        }).appendTo (panel);
    });

    // private methods
    function showPrompt (e) {
        handler ['event'] = e;
        mask.show ();
        let W = $(window).width (), H = $(window).height ();
        let w = prompt.width (),    h = $(prompt).height ();
        let x = (W - w) / 2, y = (H - h) / 2;
        prompt.css ({left:x,top:y}).show ();
    }

    function hidePrompt () {
        if (handler ['event']) {
            delete handler ['event'];
        }
        prompt.hide ();
        mask.hide ();
    }

    function findDiv(table, row, column) {
        if (row < 0 || row >= table.rows.length)
            throw 'Illegal row number: ' + row;
        if (column < 0 || column >= table.rows [row].cells.length)
            throw 'Illegal column number: ' + column;

        let td = table.rows [row].cells [column];
        if (td) {
            let div = $(td).find('div');
            if (div && div.length)
                return div;
        }
        return null;
    }

    function findParent (element, tagName) {
        if (!element || !tagName) return null;
        tagName = tagName.replace (/^\s+|\s+$/g, '').toLowerCase();
        let name = element.tagName.toLowerCase();
        if (name === tagName) return element;

        let p = element.parentNode;
        while (p) {
            name = p.tagName.toLowerCase();
            if (name === tagName) return p;
            p = p.parentNode;
        }
        return null;
    }

    function findLatitude (element) {
        let td = $(element).closest ('td'), tr = td.closest ('tr'),
            table = tr.closest ('table'), me = tables[table[0].id],
            column = td[0].cellIndex;
        if (me.selectable) column --;
        return {row: tr[0].rowIndex - me.start, column: column};
        // return {row: parseInt (td.attr ('data-row'), 10), };
    }

    function exchange (table, index1, index2) {
        let tmp, div1, div2, i,
            row1 = table.rows [index1], row2 = table.rows [index2],
            settings = tables [table.id].settings,
            length = parseInt (settings ['columns'], 10);
        for (i = 0; i < length; i ++) {
            div1 = $(row1.cells [i]).find ('>div');
            div2 = $(row2.cells [i]).find ('>div');
            tmp = div1.html ();
            div1.html (div2.html ());
            div2.html (tmp);
        }
    }

    function copyRow (src, target, length) {
        let div1, div2;
        for (let i = 0; i < length; i ++) {
            div1 = $(src.cells [i]).find ('>div');
            div2 = $(target.cells [i]).find ('>div');
            div2.html (div1.html ());
        }
    }

    function isMatrix(a) {
        if (!(a instanceof Array)) return false;
        for (let i = 0; i < a.length; i++) {
            let e = a [i];
            if (!(e instanceof Array)) return false;
        }
        return true;
    }

    function display (div, c) {
        if (c instanceof Renderer.DirectRenderer) {
            if (c.value === undefined || c.value === '') {
                div.html ('&nbsp;');
            } else {
                div.text (c.value);
            }
        } else {
            div.empty ().append (c);
        }
    }

    function createRow (tr, row, options, model, classNames) {
        let td, div, j, v, r, c;

        classNames = classNames || (model && model.table && model.table.classNames);
        row -= model.table.start;

        if (options ['selectable']) {
            td = $(tr.insertCell (-1)).addClass ('editable-cell handler-cell')
                .css ('width', 12)
                .append ($('<i class="fa fa-fw fa-angle-right"/>'))
                // .attr ('data-row-index', tr.rowIndex - model.table.start)
                .on ('click', onHandlerClicked);
        }
        // $(tr).attr ('data-row-index', tr.rowIndex - model.table.start);

        for (j = 0; j < model.getColumnCount (); j++) {
            td = $(tr.insertCell(-1)).addClass ('editable-cell').attr ({
                // 'data-row': row - model.table.start,
                'data-column': j
            });
            div = $('<div/>').appendTo(td).addClass ('data-cell fixed-length');
            // let editable = model.isCellEditable (row - model.table.start, j);

            if (classNames && classNames [j]) {
                td.addClass(classNames [j]);
                div.addClass (classNames [j]);
            }
            v = model.getValueAt (row/* - model.table.start*/, j);
            if (v === '') {
                div.html ('&nbsp;');
                div.removeAttr ('title');
            } else {
                r = model.table.getCellRenderer (row, j);
                if (r) {
                    c = r.getComponent (row, j, v);
                    display (div, c);
                } else {
                    div.text (v);
                }
                div.attr ('title', v);
            }

            if (!(r instanceof BooleanRenderer))
                div.on ('click', onCellClick);

            if (('column-size' in options) && (options['column-size'][td[0].cellIndex])) {
                div.css ('width', options ['column-size'][td[0].cellIndex]);
            }
        }
        buildCommandCell (tr, options);
        adjustColumns (tr, options);
    }

    function buildCommandCell (tr, options) {
        if (('commands' in options) && options ['commands']) {
            let td = $(tr.insertCell (-1)).addClass ('command-cell'), j,
                div = $('<div />').appendTo (td);
            let commands = [].concat (options ['commands']), a, wrap;
            for (j = 0; j < commands.length; j ++) {
                wrap = parseCommand (commands [j]);
                a = $('<a/>').appendTo (div)
                    .addClass ('et-font et-icon')
                    .addClass (wrap ['className'])
                    .on ('click', wrap ['listener']);
            }
            if (('command-cell-width' in options) && (options ['command-cell-width'] > 0)) {
                div.css ('width', options ['command-cell-width']);
            }

            return td.width ();
        }
        return -1;
    }

    function adjustColumns (row, settings) {
        if ('column-size' in settings) {
            let columnSizes = settings ['column-size'];
            for (let i = 0; i < row.cells.length; i ++) {
                let cell = $(row.cells [i]);
                cell.find ('> div').css ('width', columnSizes [i]);
            }
        }
    }

    function parseCommand (command) {
        command = (command || '').toLowerCase();
        switch (command) {
            case 'insert' : return {className: 'et-icon-plus', listener: insertRow};
            case 'remove' : return {className: 'et-icon-minus', listener: removeRow};
            case 'move-up' : return {className:'et-icon-up', listener: moveUp};
            case 'move-down' : return {className: 'et-icon-down', listener: moveDown};
            case 'move-top' : return {className:'et-icon-top', listener: moveTop};
            case 'move-bottom' : return {className:'et-icon-bottom', listener: moveBottom};
            //case 'edit-bom' : return {className:'ui-icon-gear', listener: editBom};
        }
        return {className: "", listener: null};
    }

    function onAllClick () {
        let table = $(this).closest ('table'), me = tables[table.attr ('id')],
            checked = this.checked, rows = table.find ('tr'), indexes = [];
        rows.each (function (index, row) {
            if (index > 0) {
                if (checked) {
                    $ (row).addClass ('selected-row');
                } else {
                    $ (row).removeClass ('selected-row');
                }
                indexes.push (index);
            }
        });

        if (checked) {
            if ('onRowSelect' in me.settings) {
                me.settings.onRowSelect.apply (me, indexes);
            }
        } else {
            if ('onRowUnselect' in me.settings) {
                me.settings.onRowUnselect.apply (me, indexes);
            }
        }
    }

    function createTableHeader (row, options, types, fixed) {
        let cell, classNames, headers = [], i, j, tcs = options ['tableColumns'], width;
        if ('tableColumns' in options) {
            classNames = [];
            for (j = 0; j < options ['tableColumns'].length; j ++) {
                classNames [j] = options ['tableColumns'][j]['headerClassName'] ||
                                 options ['tableColumns'][j]['className'];
            }
        }
        if (options ['selectable']) {
            cell = $(row.insertCell (-1)).addClass ('edit-table-header');
            setCellWidth (cell, options);
            $('<input/>').attr ('type', 'checkbox').appendTo (cell).on ('click', onAllClick);
        }
        if (('commands' in options) && options ['commands'] && !fixed) {
            width = buildCommandCell (row, options);
            if (width > 0) {
                row.deleteCell (row.cells.length - 1);
                options ['command-cell-width'] = width + 2;
            }
        }
        for (i = 0; i < tcs.length; i ++) {
            if (tcs [i]['header'] && tcs [i]['header'] !== '') {
                headers [i] = tcs [i]['header'];
            }
            if (tcs [i]['type'] && tcs [i]['type'] !== '') {
                types [i] = tcs [i]['type'];
            }
            cell = row.insertCell (-1);
            cell.innerHTML = '<div class="fixed-length">' + headers [i] + "</div>";
            cell.className = 'edit-table-header';
            setCellWidth (cell, options);
            if (classNames && classNames [i]) {
                cell.className += ' ' + classNames [i];
            }
        }

        if (!fixed) {
            buildCommandCell (row, options);
            options ['column-size'] = [];
            for (i = 0; i < row.cells.length; i ++) {
                options ['column-size'].push ($(row.cells [i]).width ());
            }
        } else if (('commands' in options) && options ['commands']) {
            cell = row.insertCell (-1);
            cell.className = 'edit-table-header';
            cell.colspan = options ['commands'].length;
            if (('command-cell-width' in options) && (options ['command-cell-width'] > 2))
                cell.style.width = width + 'px';
        }

        adjustColumns (row, options);
    }

    function setCellWidth (cell, options) {
        if (('column-size' in options) && (options['column-size'][cell.cellIndex])) {
            cell.style.width = options ['column-size'][cell.cellIndex] + 'px';
        }
    }

    // global events
    function onCellClick(e) {
        // onEditStop ();
        closeEditor ();
        e.stopPropagation();
        let self = $(this),
            td = self.closest ('td')[0],
            table = self.closest ('table')[0],
            id = table.id,
            me = tables [id],
            latitude = findLatitude (this),
            row = latitude ['row'],
            column = latitude ['column'],
            editor, c,
            value = me.getValueAt (latitude.row, latitude.column);

        if (me.model.isCellEditable (row, column)) {
            editor = me.getCellEditor (row, column);
            c = $(editor.getComponent());
            editor.fit (td);
            self.appendTo (handler.holder);
            editor.setValue (value);
            c.appendTo (td).show ();
            if ('onopen' in editor) {
                editor.onopen ();
            }
            handler ['table'] = {id: id, row: row, column: column, div: self};
        }
    }

    function onEditStop () {
        if (!('table' in handler))
            return;
        let backup = handler ['table'],
            me = tables [backup ['id']],
            model = me.getModel (),
            row = backup ['row'],
            column = backup ['column'],
            editor = me.getCellEditor(row, column),
            renderer = me.getCellRenderer (row, column),
            text = editor.getValue (),
            old = model.getValueAt (row, column);
        model.setValueAt (row, column, text);
        text = model.getValueAt (row, column);
        if (text === '') {
            backup ['div'].html ('&nbsp;').removeAttr ('title');
        } else {
            backup ['div'].attr ('title', text);
            if (renderer) {
                let c = renderer.getComponent (row, column, text);
                display (backup['div'], c);
            } else {
                backup ['div'].text(text);
            }
        }
        if ('onValueChanged' in me.settings) {
            me.settings ['onValueChanged'] (me, row, column, old, text);
        }
        closeEditor ();
    }

    function closeEditor () {
        if (!('table' in handler))
            return;

        let holder = handler ['table'],
            id = holder ['id'],
            row = holder ['row'],
            column = holder ['column'],
            et = tables[id],
            table = et.table,
            rowIndex = row + et.start,
            tr = table.rows [rowIndex],
            td = tr.cells [et.selectable ? column + 1 : column],
            editor = tables [id].getCellEditor (row, column),
            c = editor.getComponent();
        if ('onclose' in editor) {
            editor.onclose ();
        }
        c.appendTo(handler.holder);
        holder ['div'].appendTo (td);
        delete handler ['table'];
        delete tables [id]['editing'];
    }

    function insertRow () {
        let td = findParent (this, 'td'),
            latitude = findLatitude (this),
            table = findParent (td, 'table'),
            id = table.id,
            me = tables [id],
            row = latitude ['row'];
        me.insertRow (row);
    }

    function removeRow () {
        let tr = findParent (this, 'tr'),
            table = findParent (tr, 'table'),
            me = tables [table.id],
            canRemove = true;
        if ('beforeRowRemove' in me.settings) {
            canRemove = me.settings ['beforeRowRemove'] (me, tr.rowIndex - me.start);
        }
        if (canRemove) {
            showPrompt ({table:me, row:tr});
        }
    }

    function moveUp () {
        let latitude = findLatitude (this),
            table = findParent (this, 'table'),
            row = latitude ['row'],
            me = tables [table.id];
        if (row <= me.start) return;
        exchange (table, row, row - 1);
        me.model.moveUp (row);

        if (('onRowMoveUp' in me.settings)) {
            me.settings ['onRowMoveUp'] (me, row/* - me.start*/);
        }
    }

    function moveDown () {
        let latitude = findLatitude (this),
            table = findParent (this, 'table'),
            row = latitude ['row'],
            me = tables [table.id];
        if (row >= table.rows.length) return;
        exchange (table, row, row + 1);
        me.model.moveDown (row);
        if (('onRowMoveDown' in me.settings)) {
            me.settings ['onRowMoveDown'] (me, row);
        }
    }

    function moveTop () {
        let latitude = findLatitude (this),
            table = findParent (this, 'table'),
            row = latitude ['row'],
            me = tables [table.id], r1, r2, length;
        if (row <= me.start) return;

        r1 = table.rows [row];
        r2 = table.insertRow (me.start);
        length = parseInt (me.settings ['columns'], 10);
        createRow (r2, me.start, me.settings, me.model);
        copyRow (r1, r2, length);
        table.deleteRow (r1.rowIndex);

        me.model.moveToTop (row);

        if (('onRowMoveTop' in me.settings)) {
            me.settings ['onRowMoveTop'] (me, row);
        }
    }

    function moveBottom () {
        let latitude = findLatitude (this),
            table = findParent (this, 'table'),
            row = latitude ['row'],
            me = tables [table.id], r1, r2, length;
        if (row >= table.rows.length) return;

        r1 = table.rows [row];
        r2 = table.insertRow (-1);
        length = parseInt (me.settings ['columns'], 10);
        createRow (r2, table.rows.length - 1, me.settings, me.model);
        copyRow (r1, r2, length);
        table.deleteRow (r1.rowIndex);

        me.model.moveToBottom (row);
        if (('onRowMoveBottom' in me.settings)) {
            me.settings ['onRowMoveBottom'] (me, row);
        }
    }

    function changeTableConstruction (et) {
        let table = et.table, row, tcs = et.settings ['tableColumns'];
        $(table).removeClass ('fixed-layout');
        while (table.rows.length)
            table.deleteRow (0);

        if (tcs && tcs.length) {
            if ('column-size' in et.settings)
                delete et.settings ['column-size'];

            row = table.insertRow (-1);
            createTableHeader (row, et.settings, et.types);
            et.start = 1;

            table.deleteRow (0);
            row = table.insertRow (-1);
            createTableHeader (row, et.settings, et.types, true);
            $(table).addClass ('fixed-layout');
        }

        et.classNames = ('columnClasses' in et.settings) ? et.settings ['columnClasses'] : null;

        if ('tableColumns' in et.settings) {
            et.classNames = [];
            for (let j = 0; j < et.settings ['tableColumns'].length; j ++) {
                et.classNames [j] = et.settings ['tableColumns'][j]['className'];
            }
        }
    }

    function changeTableData  (et) {
        let table = et.table, model = et.model, i;
        while (table.rows.length > et.start)
            table.deleteRow (et.start);

        for (i = et.start; i < et.start + et.model.getRowCount (); i++) {
            et.types [i] = 'text';
            createRow (table.insertRow (-1), i, et.settings, model, et.classNames);
        }
    }

    function onHandlerClicked () {
        let self = $(this), tr = self.closest ('tr'), table = self.closest ('table'),
            me = tables [table.attr ('id')], settings = me.settings
            // index = parseInt (self.attr ('data-row-index'), 10);
        if (self.attr ('data-checked') === 'true') {
            $(tr).removeClass ('selected-row');
            if ('onRowUnselect' in settings) {
                settings ['onRowUnselect'].apply (me, [tr[0].rowIndex - me.start]);
            }
            self.removeAttr ('data-checked');
        } else {
            $(tr).addClass ('selected-row');
            if ('onRowSelect' in settings) {
                settings ['onRowSelect'].apply (me, [tr[0].rowIndex - me.start]);
            }
            self.attr ('data-checked', 'true');
        }
    }

    // inner class
    /*******************************************
     * Class TableModel
     * @constructor
     *******************************************/
    let TableModel = function (table, options) {
        this.columns = options ['columns'] || 5;
        this.rows = options ['rows'] || 1;
        this.table = table;
        this.data = [];
        for (let i = 0; i < this.rows; i ++) {
            this.data [i] = [];
        }
    };
    TableModel.prototype = {
        appendData : function (data) {
            if (!isMatrix (data))
                throw 'data must be a two-dimensional array!';

            let et = this.table, i;
            for (i = 0; i < data.length; i ++) {
                this.data.push (data [i]);
                this.rows ++;
                createRow (et.table.insertRow (-1), this.getRowCount(), et.settings, this, et.classNames);
            }
        },
        addRow : function () {
            let et = this.table;
            this.onAddRow ();
            createRow (et.table.insertRow (-1), this.getRowCount(), et.settings, this, et.classNames);
        },
        onAddRow : function () {
            this.data.push ([]);
            this.rows ++;
        },
        fireTableDataChanged : function () {
            changeTableData (this.table);
        },
        getRowCount : function () {
            return this.rows;
        },
        getColumnCount : function () {
            return this.columns;
        },
        getValueAt : function (row, column) {
            let r = this.data [row];
            return r ? r [column] : null;
        },
        setValueAt : function (row, column, value) {
            this.data [row][column] = value;
        },
        isCellEditable : function () {
            return true;
        },
        moveUp : function (row) {
            let t = this.data [row];
            this.data [row] = this.data [row - 1];
            this.data [row - 1] = t;
        },
        moveDown : function (row) {
            let t = this.data [row];
            this.data [row] = this.data [row + 1];
            this.data [row + 1] = t;
        },
        moveToTop : function (row) {
            let t = this.data [row];
            for (let i = row; i > 0; i --) {
                this.data [i] = this.data [i - 1];
            }
            this.data [0] = t;
        },
        moveToBottom : function (row) {
            let t = this.data [row];
            for (let i = row; i < this.rows - 1; i ++) {
                this.data [i] = this.data [i + 1];
            }
            this.data [this.rows - 1] = t;
        },
        insertRow : function (row) {
            for (let i = this.rows; i > row  + 1; i --) {
                this.data [i] = this.data [i - 1];
            }
            this.data [row + 1] = [];
            this.rows ++;
        },
        removeRow : function (row) {
            for (let i = row; i < this.rows; i ++) {
                this.data [i] = this.data [i + 1];
            }
            delete this.data [this.rows - 1];
            this.data.length --;
            this.rows --;
        },
        setData : function (data) {
            this.data = data;
            this.rows = data.length;
            this.fireTableDataChanged ();
        },
        getData : function () {
            return this.data;
        }
    };

    //================= renderers =================

    /*******************************************
     * Class Renderer
     * @constructor
     *******************************************/
    // todo
    let Renderer = function () {};
    Renderer.prototype = {
        getComponent : function () {
            throw "Not Implemented";
        }
    };
    Renderer.DirectRenderer = function (value) {
        this.value = value;
    }

    /*******************************************
     * Class TextRenderer
     * @constructor
     *******************************************/
    let TextRenderer = function () {
        Renderer.apply (this);
    };
    TextRenderer.prototype = $.extend ({}, Renderer.prototype, {
        getComponent : function (row, column, value) {
            return new Renderer.DirectRenderer(value);
        }
    });

    /*******************************************
     * Class SelectableRenderer
     * @constructor
     *******************************************/
    let SelectableRenderer = function (dict) {
        Renderer.apply (this);
        this.dict = dict;
        this.component = $('<div class="et-sr"/>')
            .append ($('<i class="et-font et-icon et-icon-down et-sr-icon"/>'));
        this.label = $('<span class="et-sr-label"/>').appendTo (this.component);
    };
    SelectableRenderer.prototype = $.extend ({}, Renderer.prototype, {
        getComponent : function (row, column, value) {
            this.label.text (this.dict [value]);
            return this.component;
        }
    });

    /*******************************************
     * Class BooleanRenderer
     * @constructor
     *******************************************/
    function booleanRendererClicked (e) {
        onEditStop ();
        e.stopPropagation ();
        let self = $(this), td = self.closest ('td'), icon = self.find ('.fa'),
            pos = findLatitude (this), row = pos.row, column = pos.column,
            value = icon.hasClass ('fa-square-o'), table = tables[td.closest ('table').attr ('id')];

        if (table) {
/*
            row = parseInt (row, 10);
            column = parseInt (column, 10);
*/
            if (table.model.isCellEditable (row, column)) {
                if (value) {
                    icon.removeClass ('fa-square-o').addClass ('fa-check-square-o');
                } else {
                    icon.removeClass ('fa-check-square-o').addClass ('fa-square-o');
                }
                table.model.setValueAt (row, column, value);
            }
        }
    }

    let BooleanRenderer = function () {
        Renderer.apply (this);
    }
    BooleanRenderer.prototype = $.extend ({}, Renderer.prototype, {
        getComponent : function (row, column, value) {
            let icon = $('<i class="fa fa-fw "/>')
                .addClass (value === true ? 'fa-check-square-o':'fa-square-o');
            return $('<div class="et-sr"/>').on ('click', booleanRendererClicked).append (icon);
        }
    });
    //================= end of renderers =================

    //================= editors =================
    /*******************************************
     * Class Editor
     * @constructor
     *******************************************/
    let Editor = function (type) { this.type = type; };
    Editor.prototype = {
        getComponent : function () {
            return this.component;
        },
        setValue : function (value) {},
        getValue : function () {
            return this.component.val ();
        },
        fit : function (parent) {
            let p = $(parent),
                c = $(this.component),
                d = p.find ('div');
            c.css ({
                width: d.width (),
                height: d.height ()
            }).appendTo (p);
        },
        stopEdit : onEditStop
    };

    /*******************************************
     * Class TextEditor
     * @constructor
     *******************************************/
    let TextEditor = function () {
        Editor.apply (this, ['text']);

        let self = this;
        this.component = $('<input/>').attr('type', 'text').addClass('text-editor');
        this.component.on('keyup',function (e) {
            let cell = findLatitude (this),
                table = findParent (this, 'table'),
                me = tables [table.id],
                settings = me ['settings'],
                tr, td, div;
/*
            cell.row = cell.row - me.start;
            if (me.selectable) cell.column += 1;
*/
            switch (e.keyCode) {
                case KEY_CODE.RETURN :
                    if (e.shiftKey || e.altKey) {
                        if (cell.row < table.rows.length - 1) cell.row ++;
                        if (e.shiftKey) cell.column = 0;
                        tr = table.rows [cell.row];
                        td = tr.cells [cell.column];
                        div = $(td).find ('>div');
                        onEditStop ();
                        if (div && div.length) {
                            div.trigger ('click');
                        }
                    } else {
                        onEditStop ();
                    }
                    self.value = self.component.val ();
                    break;
                case KEY_CODE.ESCAPE :
                    self.component.val (me.getValueAt (cell.row, cell.column));
                    closeEditor();
                    break;
                case KEY_CODE.LEFT :
                    if (cell.column > 0) cell.column --;
                    tr = table.rows [cell.row];
                    td = tr.cells [cell.column];
                    div = $(td).find ('>div');
                    onEditStop ();
                    if (div && div.length) {
                        div.trigger ('click');
                    }
                    break;
                case KEY_CODE.UP :
                    if (cell.row > 0) cell.row --;
                    tr = table.rows [cell.row];
                    td = tr.cells [cell.column];
                    div = $(td).find ('>div');
                    onEditStop ();
                    if (div && div.length) {
                        div.trigger ('click');
                    }
                    break;
                case KEY_CODE.RIGHT :
                    if (cell.column < settings ['columns']) cell.column ++;
                    tr = table.rows [cell.row];
                    td = tr.cells [cell.column];
                    div = $(td).find ('>div');
                    onEditStop ();
                    if (div && div.length) {
                        div.trigger ('click');
                    }
                    break;
                case KEY_CODE.DOWN :
                    if (cell.row < table.rows.length - 1) cell.row ++;
                    tr = table.rows [cell.row];
                    td = tr.cells [cell.column];
                    div = $(td).find ('>div');
                    onEditStop ();
                    if (div && div.length) {
                        div.trigger ('click');
                    }
                    break;
                default :
                    // todo: value check
//                    let type = me.getCellType (cell.row, cell.column);
/*
                    if (self.type == 'number') {
                        let value = self.component.value;
                        if (value && !/^[\d]*(\.[\d]+)?$/ig.test (self.component.value)) {
                            self.component.value = value.substring (0, value.length - 1);
                        }
                    }
*/
                    break;
            }
        }).on('click',function (e) {
            e.stopPropagation();
        }).on ('change', function () {
            onEditStop ();
        });
    };
    TextEditor.prototype = $.extend ({}, Editor.prototype, {
        setValue : function (value) {
            this.component.val (value === undefined ? '' : value.replace (/&nbsp;/, ''));
        },
        onopen : function () {
            this.component.focus ().select ();
        },
        onclose : function () {
            this.component.trigger ('blur');
        }
    });

    /*******************************************
     * Class DropdownEditor
     * @constructor
     *******************************************/
    function onListItemClicked (e) {
        e.stopPropagation ();
        let self = $(this), ul = self.closest ('ul'), value = self.attr ('data-value'),
            editor = ul.data ('editor');
        editor.selectedValue = value;
        ul.hide ();
        onEditStop ();
    }
    function cancelDropdown (e) {
        if (e.keyCode === KEY_CODE.ESCAPE) {
            closeEditor ();
        }
    }
    let SelectableEditor = function (data) {
        Editor.apply (this, ['dropdown']);
        let div = $('<div class="data-cell fixed-length"/>');
        let label = $('<div class="et-sr"/>').append ('<i class="et-font et-icon et-icon-up et-sr-icon"/>').appendTo (div);
        let root = $('<ul class="et-list-editor"/>').data ('editor', this),
            i, o, k, v;
        $(function () {
            root.appendTo ($(document.body));
        })
        if (data instanceof Array) {
            for (i = 0; i < data.length; i ++) {
                o = data [i];
                if (!('value' in o) && !('text' in o)) {
                    throw 'Invalid argument: data must be an array of {value:xxx,text:yyy}';
                }
                $('<li class="et-list-item"/>').attr ('data-value', o['value'])
                    .on ('click', onListItemClicked)
                    .append ($('<a class="et-list-handler text-editor"/>').text (o['text']))
                    .appendTo (root);
            }
        } else {
            for (k in data) if (data.hasOwnProperty (k)) {
                v = data [k];
                $('<li class="et-list-item"/>').attr ('data-value', k)
                    .on ('click', onListItemClicked)
                    .append ($('<a class="et-list-handler text-editor"/>').text (v))
                    .appendTo (root);
            }
        }

        this.selectedIndex = -1;
        this.selectedValue = undefined;
        this.component = div.on ('click', function (e) {
            e.stopPropagation ();
            root.hide ();
            closeEditor ();
        });
        this.list = root;
        this.title = $('<span class="et-sr-label"/>').appendTo (label);
    };
    SelectableEditor.prototype = $.extend ({}, Editor.prototype, {
        setValue : function (value) {
            let self = this, label, bill, i, item;
            this.selectedValue = value;
            bill = this.list.find ('.et-list-item').removeClass ('active');
            for (i = 0; i < bill.length; i ++) {
                item = $(bill[i]);
                if (value === item.attr ('data-value')) {
                    label = item.find ('a').text ();
                    item.addClass ('active');
                    this.selectedIndex = i;

                    break;
                }
            }
            this.title.text (label);
        },
        getValue : function () {
            return this.selectedValue;
        },
        fit : function (parent) {
            let p = $(parent), w = p.width (), h = p.outerHeight (), pos = p.offset ();
            this.list.css ({
                left: pos.left, top: pos.top + h, width: w, height: 'auto'
            }).show ();
        },
        getComponent () {
            return this.component;
        },
        onopen : function () {
/*
            this.selectedValue = undefined;
            this.selectedIndex = -1;
*/
            $(document).on ('keyup', cancelDropdown);
        },
        onclose : function () {
            this.list.hide ();
            this.selectedValue = undefined;
            this.selectedIndex = -1;

            $(document).unbind ('keyup', cancelDropdown);
        }
    });

    /*******************************************
     * Class BooleanEditor
     * @constructor
     *******************************************/
    let BooleanEditor = function () {
        Editor.apply (this, ['boolean']);
    };
    BooleanEditor.prototype = $.extend ({}, Editor.prototype, {
        setValue: function (value) { this.value = value; },
        getValue: function () { return this.value }
    });
    //================= end of editors =================

    let Default_Editors = {
        text : new TextEditor ()
    }, Default_Renderer = {
        text : new TextRenderer (),
        select: new SelectableRenderer (),
        boolean: new BooleanRenderer ()
    };

    /*******************************************
     * Class EditableTable
     * @constructor
     *******************************************/
    function ET (options) {
        if (!('container' in options))
            throw 'container must set';

        this.settings = $.extend({}, defaults, options || {});
        this.table = document.createElement('table');
        $(this.settings ['container']).empty ().append ($(this.table));
        this.table.className = 'editable-table';
        this.id = this.table.id = 'ET_' + ('' + Math.random()).substring(2);
        this.model = options ['model'] || new TableModel (this, options);
        this.types = [];
        this.start = 0;
        this.selectable = this.settings.selectable = options ['selectable'] || false;

        changeTableConstruction (this);
        changeTableData (this);
    }

    ET.prototype = {
        // public methods
        appendData : function (data) {
            this.model.appendData (data);
        },
        addRow : function () {
            this.model.addRow ();
        },
        highlightRows : function () {
            let a, i, j, r;
            if (!('highlightClassName' in this.settings))
                return;
            let cn = this.settings ['highlightClassName'];
            for (i = 0; i < arguments.length; i ++) {
                a = arguments [i];
                if (a instanceof Array) {
                    for (j = 0; j < a.length; j ++) {
                        r = this.table.rows [a [j] + this.start];
                        if (r)
                            $(r).addClass (cn);
                    }
                } else {
                    r = this.table.rows [a + this.start];
                    if (r)
                        $(r).addClass (cn);
                }
            }
        },
        clearHighlight : function () {
            if (!('highlightClassName' in this.settings))
                return;
            let cn = this.settings ['highlightClassName'], i, j, a, r;

            if (typeof arguments [0] == 'boolean') {
                if (arguments [0]) {
                    for (i = this.start; i < this.table.rows.length; i ++) {
                        $(this.table.rows [i]).removeClass (cn);
                    }
                }
            } else {
                for (i = 0; i < arguments.length; i ++) {
                    a = arguments [i];
                    if (a instanceof Array) {
                        for (j = 0; j < a.length; j ++) {
                            r = this.table.rows [a [j] + this.start];
                            if (r)
                                $(r).removeClass (cn);
                        }
                    } else {
                        r = this.table.rows [a + this.start];
                        if (r)
                            $(r).removeClass (cn);
                    }
                }
            }
        },
        getValueAt : function (row, column) {
            return this.model.getValueAt (row, column);
        },
        setValueAt : function (row, column, value) {
            this.model.setValueAt (row, column, value);
            let me = tables[this.id], table = me.table, tr = table.rows[row + me.start],
                td = tr.cells [column + (me.selectable ? 1 : 0)],
                div = $(td).find ('>div'), renderer = this.getCellRenderer (row, column),
                c = renderer.getComponent(row, column, value);

            display (div, c);
        },
        getCellType : function (row, column) {
            let func = this.settings ['getCellType'];
            if (typeof func == 'function') {
                return func.call(this, row, column);
            }

            if ('columnTypes' in this.settings) {
                let types = this.settings ['columnTypes'];
                if (types instanceof Array) {
                    if (isMatrix(types)) {
                        return types [row + this.start][column];
                    } else {
                        return types [column];
                    }
                } else if (typeof types == 'string') {
                    return types;
                }
            }

            return 'text';
        },
        getCellEditor : function (row, column) {
            let func = this.settings ['getCellEditor'], editor;
            if (typeof func == 'function') {
                editor = func.call (this, row, column);
            }
            if (editor) return editor;
            if ('columnTypes' in this.settings) {
                let types = this.settings ['columnTypes'], type;
                if (types instanceof Array) {
                    if (isMatrix(types)) {
                        type = types [row + this.start][column];
                    } else {
                        type = types [column];
                    }
                } else if (typeof types == 'string') {
                    type = types;
                }

                editor = Default_Editors [type];
            }

            return editor || Default_Editors ['text'];
        },
        getCellRenderer : function (row, column) {
            let func = this.settings ['getCellRenderer'], renderer;
            if (typeof func === 'function') {
                renderer = func.call (this, row, column);
            }
            if (renderer) return renderer;

            if ('columnTypes' in this.settings) {
                let types = this.settings ['columnTypes'], type;
                if (types instanceof Array) {
                    if (isMatrix (types)) {
                        type = types [row + this.start][column];
                    } else {
                        type = types [column];
                    }
                } else if (typeof  types === 'string') {
                    type = types;
                }

                renderer = Default_Renderer [type];
            }

            return renderer || Default_Renderer.text;
        },
        getValue : function () {
            return this.model.data;
        },
        dispose : function () {
            $(this.settings ['container']).empty ();
            delete this.table;
            delete tables [this.id];
            delete this.model;
        },
        getModel : function () {
            return this.model;
        },
        insertRow : function (row) {
            let tr = this.table.insertRow (row + 1);
            this.model.insertRow (row - this.start);
            createRow (tr, row + 1, this.settings, this.model);

            if (this.settings && ('onRowInsert' in this.settings)) {
                this.settings ['onRowInsert'] (this);
            }
        },
        removeRow : function (row) {
            let model = this.model, settings = this.settings, table = this.table,
                tr = table.rows [row + this.start];
            table.deleteRow (tr.rowIndex);
            model.removeRow (row);

            if (('onRowRemove' in settings)) {
                settings ['onRowRemove'] (this, row);
            }
        },
        getSelectedRowIndexes : function () {
            let a = [], table = $('table[id=' + this.id + ']'), me = tables[this.id];
            table.find ('tr.selected-row').each (function () {
                a.push (this.rowIndex - me.start);
            });
            return a;
        },
        editBom : function (row) {
        	let model = this.model;
        	model.editBom (row);
        }
    };

/*
    $(document).on ('click', function () {
        onEditStop ();
    });
*/
    return {
        TableModel : TableModel,
        Editors : {
            AbstractEditor : Editor,
            TextEditor     : TextEditor,
            SelectableEditor: SelectableEditor,
            BooleanEditor: BooleanEditor
        },
        Renderer: {
            AbstractRenderer : Renderer,
            SelectableRenderer: SelectableRenderer,
            BooleanRenderer: BooleanRenderer
        },
        install: function (options) {
            this.stopOn (document)
            let et = new ET (options);
            return tables [et.id] = et;
        },
        stopOn : function () {
            let args = arguments;
            $(function () {
                for (let i = 0; i < args.length; i ++) {
                    $(args [i]).on ('click', onEditStop);
                }
            });
        }
    };
})(jQuery);